The data type ID (in DataTypeId Property) is an optional information in the UAGenericObject Class (it equals to UAModelNodeDescriptor.Null when "not present"). It is basically a node ID of data type that this UAGenericObject represents.
Show UAGenericObject class diagram
For reads, writes and subscriptions (but not for method calls or some other scenarios), the data type ID of certain node can be determined from the information model of the server (by reading the DataType attribute of that node). That is, however, the declared data type for that node. As the OPC UA information model for data types follows the classic paradigms of OOP (Object Oriented Programming), the actual data type at runtime can also be any direct or indirect sub-type of the declared data type (in fact, when the declared data type is abstract, the actual data type must be some sub-type, different from the declared type - this is again the same as in classic OOP, because instances of abstract types cannot be created).
In many cases, the node in the OPC UA server you are dealing with has a data type that has no sub-types. In such case, you are lucky, because you always know upfront the precise data type that is to be used for the node's value. But in some cases, the situation is more complicated, and the problem domain calls for sub-types, a hierarchical structure of data types. The picture below is a class diagram that shows example relations between three data types. The ScalarValueDataType and ArrayValueDataType inherit from a base data type, Structure.
When a node in such server then specifies its data type as Structure, there are three possible data types that the value can actually have: Structure, ScalarValueDataType, or ArrayValueDataType.
When the generic object is obtained from the server (such as by reads and subscriptions), the component always fills in the with a data type ID that is not UAModelNodeDescriptor.Null. When the generic object is to be transferred to the server (such as by writing), the component will use the data type ID provided, if it is not equal to UAModelNodeDescriptor.Null. Otherwise, the component will try to determine the data type (ID) itself, which may not work in case the actual data to be written are a sub-type of the node's data type (and which is always the case if the node's data type is abstract).
What do you need to take out of this discussion? That's actually easy:
How you obtain the data type ID depends on the application. For example
The example below determines sub-types of a given data type:
Note that this example shows both abstract and concrete data types. Only the concrete (= non-abstract) data types can be used for data transfer. You can determine whether a data type is abstract by reading the IsAbstract attribute of its data type ID node.